#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Flask web application for Item-Based Collaborative Filtering Recommendation System.

Created on: 2025-04-20
Author: Nianqing Liu
"""

import os
import sys
import matplotlib

matplotlib.use("Agg")  # Use non-interactive backend
import matplotlib.pyplot as plt
from flask import (
    Flask,
    render_template,
    request,
    redirect,
    url_for,
    flash,
    jsonify,
    send_file,
)
import time
import traceback


# Add parent directory to path so we can import from src
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from src.data_loader import DataLoader
from src.model import ItemBasedCF
from src.logger import setup_logger

# Initialize Flask app
app = Flask(__name__)
app.secret_key = os.urandom(24)

# Setup logger
logger = setup_logger(log_dir="../results/logs", log_level="INFO")

# Global variables
data_loader = None
model = None
MODEL_PATH = "../models/item_based_cf.pkl"
DATA_PATH = "../data/ml-100k"
OUTPUT_DIR = "../results"

# Create directories if they don't exist
os.makedirs(os.path.dirname(MODEL_PATH), exist_ok=True)
os.makedirs(os.path.join(OUTPUT_DIR, "images"), exist_ok=True)


def initialize_data_and_model():
    """Initialize the data loader and model."""
    global data_loader, model

    logger.info("Initializing data and model...")

    # Initialize data loader and load data
    if data_loader is None:
        try:
            data_loader = DataLoader(data_path=DATA_PATH)
            ratings_df, movies_df = data_loader.load_data()
            logger.info(
                f"Data loaded: {len(ratings_df)} ratings, {len(movies_df)} movies"
            )
        except Exception as e:
            logger.error(f"Error loading data: {str(e)}")
            logger.error(traceback.format_exc())
            return False

    # Initialize model if not already loaded
    if model is None:
        try:
            # Check if pre-trained model exists
            if os.path.exists(MODEL_PATH):
                logger.info(f"Loading pre-trained model from {MODEL_PATH}")
                model = ItemBasedCF.load_model(
                    MODEL_PATH, movies_df=data_loader.movies_df
                )
            else:
                logger.info("Training new model with default parameters")
                model = ItemBasedCF(
                    similarity_method="adjusted_cosine",
                    prediction_method="weighted_average_with_baseline",
                    k=30,
                )
                model.fit(data_loader.ratings_df, data_loader.movies_df, test_size=0)
                # Save model for future use
                model.save_model(MODEL_PATH)
                logger.info(f"Model trained and saved to {MODEL_PATH}")
        except Exception as e:
            logger.error(f"Error initializing model: {str(e)}")
            logger.error(traceback.format_exc())
            return False

    return True


def visualize_recommendations(user_id, recommendations):
    """
    Create a visualization of recommendations for a user.

    Parameters:
    -----------
    user_id : int
        User ID
    recommendations : pandas.DataFrame
        DataFrame containing recommendations

    Returns:
    --------
    str
        Path to the generated image
    """
    if recommendations is None or recommendations.empty:
        return None

    plt.figure(figsize=(10, 6))

    # Sort by predicted rating
    sorted_recs = recommendations.sort_values("predicted_rating", ascending=True)

    # Limit to top 10 for better visualization
    if len(sorted_recs) > 10:
        sorted_recs = sorted_recs.tail(10)

    # Shorten movie titles if too long
    sorted_recs["display_title"] = sorted_recs["movie_title"].apply(
        lambda x: x[:30] + "..." if len(x) > 30 else x
    )

    # Plot horizontal bar chart
    plt.barh(
        sorted_recs["display_title"], sorted_recs["predicted_rating"], color="skyblue"
    )
    plt.xlabel("Predicted Rating")
    plt.title(f"Movie Recommendations for User {user_id}")
    plt.xlim(0, 5.5)
    plt.grid(axis="x", linestyle="--", alpha=0.7)
    plt.tight_layout()

    # Save the figure
    output_path = os.path.join(OUTPUT_DIR, f"images/user_{user_id}_recommendations.png")
    plt.savefig(output_path)
    plt.close()

    # Return the relative path
    return f"/results/images/user_{user_id}_recommendations.png"


def visualize_similar_items(item_id, similar_items):
    """
    Create a visualization of items similar to a given item.

    Parameters:
    -----------
    item_id : int
        Item ID
    similar_items : pandas.DataFrame
        DataFrame containing similar items

    Returns:
    --------
    str
        Path to the generated image
    """
    if similar_items is None or similar_items.empty:
        return None

    plt.figure(figsize=(10, 6))

    # Sort by similarity
    sorted_items = similar_items.sort_values("similarity", ascending=True)

    # Limit to top 10 for better visualization
    if len(sorted_items) > 10:
        sorted_items = sorted_items.tail(10)

    # Shorten movie titles if too long
    sorted_items["display_title"] = sorted_items["movie_title"].apply(
        lambda x: x[:30] + "..." if len(x) > 30 else x
    )

    # Plot horizontal bar chart
    plt.barh(
        sorted_items["display_title"], sorted_items["similarity"], color="lightgreen"
    )
    plt.xlabel("Similarity Score")
    plt.title(f"Movies Similar to Item {item_id}")
    plt.xlim(0, 1.05)
    plt.grid(axis="x", linestyle="--", alpha=0.7)
    plt.tight_layout()

    # Save the figure
    output_path = os.path.join(OUTPUT_DIR, f"images/item_{item_id}_similar_items.png")
    plt.savefig(output_path)
    plt.close()

    # Return the relative path
    return f"/results/images/item_{item_id}_similar_items.png"


@app.route("/")
def index():
    """Render the home page."""
    # Initialize data and model
    if not initialize_data_and_model():
        flash("Error initializing data and model. Please check logs.", "danger")

    # Get model parameters if model is initialized
    model_params = {}
    if model and model.is_trained:
        model_params = {
            "similarity_method": model.similarity_method,
            "prediction_method": model.prediction_method,
            "k": model.k,
            "n_users": model.n_users,
            "n_items": model.n_items,
        }

    # Get available similarity and prediction methods
    similarity_methods = ["cosine", "pearson", "euclidean", "adjusted_cosine"]
    prediction_methods = ["simple_weighted", "weighted_average_with_baseline"]

    # Get dataset statistics
    dataset_stats = {}
    if data_loader:
        dataset_stats = data_loader.get_dataset_stats()

    return render_template(
        "index.html",
        model_params=model_params,
        similarity_methods=similarity_methods,
        prediction_methods=prediction_methods,
        dataset_stats=dataset_stats,
    )


@app.route("/results/images/<filename>")
def serve_image(filename):
    """Serve images from the results directory."""
    image_path = os.path.join(
        os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
        "results",
        "images",
        filename,
    )
    return send_file(image_path, mimetype="image/png")


@app.route("/recommend", methods=["GET", "POST"])
def recommend():
    """Generate recommendations based on form input or URL parameters."""
    global model  # Declare model as global at the start of the function

    # Initialize data and model first
    if not initialize_data_and_model():
        flash("Error initializing data and model. Please check logs.", "danger")
        return redirect(url_for("index"))

    # Now model should be initialized, extract current model parameters for defaults
    current_similarity = "adjusted_cosine"
    current_prediction = "weighted_average_with_baseline"
    current_k = 30

    if model:
        current_similarity = model.similarity_method or "adjusted_cosine"
        current_prediction = model.prediction_method or "weighted_average_with_baseline"
        current_k = model.k or 30

    # Determine if request is GET or POST and extract parameters accordingly
    if request.method == "POST":
        # Get form data
        user_id = int(request.form.get("user_id", 1))
        num_recommendations = int(request.form.get("num_recommendations", 10))
        similarity_method = request.form.get("similarity_method", current_similarity)
        prediction_method = request.form.get("prediction_method", current_prediction)
        k = int(request.form.get("k", current_k))
    else:  # GET request
        # Get URL parameters
        user_id = int(float(request.args.get("user_id", "1")))
        # Validate user_id is in valid range
        if user_id < 1 or user_id > model.n_users:
            flash(
                f"Invalid user ID: {user_id}. Must be between 1 and {model.n_users}",
                "danger",
            )
            return redirect(url_for("index"))
        num_recommendations = int(request.args.get("num_recommendations", 10))
        similarity_method = request.args.get("similarity_method", current_similarity)
        prediction_method = request.args.get("prediction_method", current_prediction)
        k = int(request.args.get("k", current_k))

    # Ensure parameters are not None
    similarity_method = similarity_method or "adjusted_cosine"
    prediction_method = prediction_method or "weighted_average_with_baseline"
    k = k or 30

    logger.info(
        f"Recommendation parameters: user_id={user_id}, similarity={similarity_method}, prediction={prediction_method}, k={k}"
    )

    # Check if we need to retrain the model
    retrain = False
    if model and (
        model.similarity_method != similarity_method
        or model.prediction_method != prediction_method
        or model.k != k
    ):
        retrain = True

    # Retrain if needed
    if retrain:
        try:
            logger.info(
                f"Retraining model with similarity={similarity_method}, prediction={prediction_method}, k={k}"
            )
            model = ItemBasedCF(
                similarity_method=similarity_method,
                prediction_method=prediction_method,
                k=k,
            )
            model.fit(data_loader.ratings_df, data_loader.movies_df, test_size=0)
            # Save the retrained model
            model.save_model(MODEL_PATH)
            logger.info("Model retrained and saved")
        except Exception as e:
            logger.error(f"Error retraining model: {str(e)}")
            logger.error(traceback.format_exc())
            flash(f"Error retraining model: {str(e)}", "danger")
            return redirect(url_for("index"))

    # Generate recommendations
    try:
        start_time = time.time()
        recommendations = model.recommend(user_id, top_n=num_recommendations)
        generation_time = time.time() - start_time

        # Visualize recommendations
        img_path = None
        if not recommendations.empty:
            img_path = visualize_recommendations(user_id, recommendations)

        # Get user's current ratings if available
        user_ratings = None
        if data_loader:
            try:
                user_ratings = data_loader.get_user_ratings(user_id)
                # Sort by rating (descending)
                user_ratings = user_ratings.sort_values("rating", ascending=False)
            except:
                pass

        # Get top rated items
        top_rated_items = None
        try:
            top_rated_items = model.get_user_top_rated(user_id, min_rating=4.0, top_n=5)
        except:
            pass

        return render_template(
            "recommendations.html",
            user_id=user_id,
            recommendations=recommendations,
            generation_time=generation_time,
            img_path=img_path,
            user_ratings=user_ratings,
            top_rated_items=top_rated_items,
            model_params={
                "similarity_method": model.similarity_method,
                "prediction_method": model.prediction_method,
                "k": model.k,
            },
        )
    except Exception as e:
        logger.error(f"Error generating recommendations: {str(e)}")
        logger.error(traceback.format_exc())
        flash(f"Error generating recommendations: {str(e)}", "danger")
        return redirect(url_for("index"))


@app.route("/similar_items", methods=["GET", "POST"])
def similar_items():
    """Find items similar to a given item."""
    global model

    # Initialize data and model first
    if not initialize_data_and_model():
        flash("Error initializing data and model. Please check logs.", "danger")
        return redirect(url_for("index"))

    # Get item_id from form or URL
    if request.method == "POST":
        item_id = int(request.form.get("item_id", 1))
        num_similar = int(request.form.get("num_similar", 10))
    else:  # GET request
        item_id = int(request.args.get("item_id", 1))
        num_similar = int(request.args.get("num_similar", 10))

    # Validate item_id is in valid range
    if item_id < 1 or item_id > model.n_items:
        flash(
            f"Invalid item ID: {item_id}. Must be between 1 and {model.n_items}",
            "danger",
        )
        return redirect(url_for("index"))

    # Get similar items
    try:
        start_time = time.time()
        similar_items_df = model.get_similar_items(item_id, top_n=num_similar)
        generation_time = time.time() - start_time

        # Visualize similar items
        img_path = None
        if not similar_items_df.empty:
            img_path = visualize_similar_items(item_id, similar_items_df)

        # Get item information
        item_info = None
        if data_loader and data_loader.movies_df is not None:
            item_info = data_loader.movies_df[
                data_loader.movies_df["movie_id"] == item_id
            ]

        # Get users who rated this item
        item_ratings = None
        if data_loader:
            try:
                item_ratings = data_loader.get_item_ratings(item_id)
                # Sort by rating (descending)
                item_ratings = item_ratings.sort_values("rating", ascending=False)
            except:
                pass

        return render_template(
            "similar_items.html",
            item_id=item_id,
            similar_items=similar_items_df,
            generation_time=generation_time,
            img_path=img_path,
            item_info=item_info,
            item_ratings=item_ratings,
            model_params={
                "similarity_method": model.similarity_method,
                "k": model.k,
            },
        )
    except Exception as e:
        logger.error(f"Error finding similar items: {str(e)}")
        logger.error(traceback.format_exc())
        flash(f"Error finding similar items: {str(e)}", "danger")
        return redirect(url_for("index"))


@app.route("/more_like_this", methods=["GET", "POST"])
def more_like_this():
    """Find items similar to a given item that a user might like."""
    global model

    # Initialize data and model first
    if not initialize_data_and_model():
        flash("Error initializing data and model. Please check logs.", "danger")
        return redirect(url_for("index"))

    # Get parameters
    if request.method == "POST":
        user_id = int(request.form.get("user_id", 1))
        item_id = int(request.form.get("item_id", 1))
        num_recommendations = int(request.form.get("num_recommendations", 10))
    else:  # GET request
        user_id = int(request.args.get("user_id", 1))
        item_id = int(request.args.get("item_id", 1))
        num_recommendations = int(request.args.get("num_recommendations", 10))

    # Validate IDs
    if user_id < 1 or user_id > model.n_users:
        flash(
            f"Invalid user ID: {user_id}. Must be between 1 and {model.n_users}",
            "danger",
        )
        return redirect(url_for("index"))

    if item_id < 1 or item_id > model.n_items:
        flash(
            f"Invalid item ID: {item_id}. Must be between 1 and {model.n_items}",
            "danger",
        )
        return redirect(url_for("index"))

    # Generate "more like this" recommendations
    try:
        start_time = time.time()
        recommendations = model.more_like_this(
            user_id, item_id, top_n=num_recommendations
        )
        generation_time = time.time() - start_time

        # Visualize recommendations
        img_path = None
        if not recommendations.empty:
            img_path = visualize_recommendations(user_id, recommendations)

        # Get item information
        item_info = None
        if data_loader and data_loader.movies_df is not None:
            item_info = data_loader.movies_df[
                data_loader.movies_df["movie_id"] == item_id
            ]

        # Get user's rating for this item
        user_rating = None
        if data_loader:
            user_ratings = data_loader.ratings_df[
                (data_loader.ratings_df["user_id"] == user_id)
                & (data_loader.ratings_df["item_id"] == item_id)
            ]
            if not user_ratings.empty:
                user_rating = user_ratings.iloc[0]["rating"]

        return render_template(
            "more_like_this.html",
            user_id=user_id,
            item_id=item_id,
            recommendations=recommendations,
            generation_time=generation_time,
            img_path=img_path,
            item_info=item_info,
            user_rating=user_rating,
            model_params={
                "similarity_method": model.similarity_method,
                "k": model.k,
            },
        )
    except Exception as e:
        logger.error(f"Error generating 'more like this' recommendations: {str(e)}")
        logger.error(traceback.format_exc())
        flash(f"Error generating 'more like this' recommendations: {str(e)}", "danger")
        return redirect(url_for("index"))


@app.route("/evaluate", methods=["POST"])
def evaluate_model():
    """Evaluate the model and show results."""
    global model  # Declare model as global at the start of the function

    # Initialize data and model
    if not initialize_data_and_model():
        flash("Error initializing data and model. Please check logs.", "danger")
        return redirect(url_for("index"))

    # Train a model with test split if needed
    try:
        logger.info("Training model with test split for evaluation")
        similarity_method = request.form.get("similarity_method", "adjusted_cosine")
        prediction_method = request.form.get(
            "prediction_method", "weighted_average_with_baseline"
        )
        k = int(request.form.get("k", 30))
        test_size = 0.2

        model = ItemBasedCF(
            similarity_method=similarity_method,
            prediction_method=prediction_method,
            k=k,
        )
        model.fit(data_loader.ratings_df, data_loader.movies_df, test_size=test_size)

        # Evaluate model
        start_time = time.time()
        metrics = model.evaluate()
        evaluation_time = time.time() - start_time
        # Create empty recommendations DataFrame to avoid template error
        import pandas as pd

        recommendations = pd.DataFrame()

        return render_template(
            "evaluation.html",
            metrics=metrics,
            evaluation_time=evaluation_time,
            model_params={
                "similarity_method": model.similarity_method,
                "prediction_method": model.prediction_method,
                "k": model.k,
                "test_size": test_size,
            },
            recommendations=recommendations,  # Add this line to fix the error
        )
    except Exception as e:
        logger.error(f"Error evaluating model: {str(e)}")
        logger.error(traceback.format_exc())
        flash(f"Error evaluating model: {str(e)}", "danger")
        return redirect(url_for("index"))


@app.route("/api/users", methods=["GET"])
def get_users():
    """API endpoint to get a list of users."""
    if not initialize_data_and_model():
        return jsonify({"error": "Failed to initialize data and model"}), 500

    try:
        # Get a list of all user IDs
        user_ids = sorted(data_loader.ratings_df["user_id"].unique().tolist())
        return jsonify({"users": user_ids})
    except Exception as e:
        logger.error(f"Error getting users: {str(e)}")
        return jsonify({"error": str(e)}), 500


@app.route("/api/items", methods=["GET"])
def get_items():
    """API endpoint to get a list of items."""
    if not initialize_data_and_model():
        return jsonify({"error": "Failed to initialize data and model"}), 500

    try:
        # Get a list of all item IDs with titles
        items = data_loader.movies_df[["movie_id", "movie_title"]].to_dict(
            orient="records"
        )
        return jsonify({"items": items})
    except Exception as e:
        logger.error(f"Error getting items: {str(e)}")
        return jsonify({"error": str(e)}), 500


@app.route("/api/popular_movies", methods=["GET"])
def get_popular_movies():
    """API endpoint to get popular movies."""
    if not initialize_data_and_model():
        return jsonify({"error": "Failed to initialize data and model"}), 500

    try:
        # Use the built-in function to get popular items
        from src.recommendation import get_popular_items

        popular_movies = get_popular_items(
            model.ratings_matrix, data_loader.movies_df, top_n=10
        )

        # Convert to dictionary for JSON response
        movies_list = popular_movies[
            ["movie_id", "movie_title", "rating_count", "avg_rating"]
        ].to_dict(orient="records")
        return jsonify({"popular_movies": movies_list})
    except Exception as e:
        logger.error(f"Error getting popular movies: {str(e)}")
        return jsonify({"error": str(e)}), 500


if __name__ == "__main__":
    # Initialize data and model at startup
    initialize_data_and_model()

    # Run Flask app
    app.run(debug=True, host="0.0.0.0", port=5000)
